"""
ForgeFed plugin for Pagure.
Copyright (C) 2020-2021 zPlus <zplus@peers.community>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.

SPDX-FileCopyrightText:  2020-2021 zPlus <zplus@peers.community>
SPDX-License-Identifier: GPL-2.0-only
"""

import celery
import json
import os
import pagure

from .. import APP_URL
from .. import activitypub
from .. import feeds
from .. import model
from .. import settings
from . import broker
from . import broker_url
from . import database_session

log = celery.utils.log.get_task_logger(__name__)
log.setLevel(settings.LOG_LEVEL)

# The following is a decorator that accepts a Pagure notification ID as input,
# for example "issue.new", and adds it to _USER_ACTIONS_ together with the
# decorated function. _USER_ACTIONS_ is a dictionary mapping Pagure notification
# IDs to a function to be executed when a new notification is received.
_USER_ACTIONS_ = {}
def action(notification_id):
    def closure(func):

        def decorator(*args, **kwargs):
            func(*args, **kwargs)

        global _USER_ACTIONS_
        _USER_ACTIONS_[notification_id] = decorator

        return decorator
    return closure

@broker.task
def handle_pagure_signal(notification_id, message, forgefed_worker):
    """
    This task receives notifications from Pagure about events that happen on
    the instance, creates a new activity, and schedules their delivery.

    :param forgefed_worker: The value of envvar FORGEFED_WORKER of the process
        that scheduled the task.
    """

    if forgefed_worker.upper() == 'TRUE':
        log.debug('Ignoring notification {} in ForgeFed worker thread.'.format(notification_id))
        return

    if notification_id not in _USER_ACTIONS_:
        log.debug('Unhandled user action {}.'.format(notification_id))
        return

    log.debug('New Pagure notification: {}\n{}'.format(
        notification_id, json.dumps(message, indent=4, sort_keys=True)))

    with database_session() as database:

        # Handle the user action
        return _USER_ACTIONS_[notification_id](database, message)




# Git
# -----------------------------------------------------------------------------

@action('git.receive')
def git_commit(database, message):
    """
    A user has committed something to a Repository.
    """

    # This is the person that has pushed the commits, not the author
    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['repo']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and repository

    if repository.is_remote:

        log.debug('Local users should not push to remote repositories. '
                  'Open a MergeRequest instead. If you have commit access to a '
                  'remote repository, you should push to its URL.')
        return

    log.debug('Sending Push notification to Repository {} followers.'.format(repository.uri))

    repository_jsonld = activitypub.fetch(repository.local_uri)

    # TODO The Pagure notification only gives us the start_commit and end_commit.
    #      We cannot rely on fetching from the repo all the commits between these
    #      two, because there could be other commits in between, pushed earlier.
    #      We've got to modify the Pagure notification (in the Pagure codebase)
    #      to return the entire list of commits instead of only 2.
    commits_uri = []
    for hash in [ message['start_commit'], message['end_commit'] ]:
        commits_uri.append('{}/c/{}'.format(project.uri, hash))

    activitypub.Activity(
        type   = 'Push',
        actor  = person.uri,
        object = {
            'type': 'OrderedCollection',
            'totalItems': message['total_commits'],
            'items': list(set(commits_uri))
        },
        to = repository_jsonld['followers'],
        target = activitypub.Branch(project, repository, message['branch'])['id'],
        context = repository.uri
    ).distribute()

@action('git.tag.creation')
def git_tag(database, message):
    """
    A user has tagged a Repository.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['repo']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and repository and person

    if repository.is_remote:
        log.debug('Local users should not push to remote repositories. '
                  'If you have commit access to a remote repository, you should '
                  'push to its URL.')
        return

    log.debug('Sending Create(Tag) notification to Repository {} followers.'.format(repository.uri))

    ref = activitypub.TagRef(repository, 'refs/tags/{}'.format(message['tag']))
    repository_jsonld = activitypub.fetch(repository.local_uri)

    activitypub.Activity(
        type = 'Create',
        actor = person.uri,
        object = ref['id'],
        to = repository_jsonld['followers']
    ).distribute()




# Actor
# -----------------------------------------------------------------------------

@action('forgefed.follow')
def follow(database, message):
    """
    An Actor has followed another Actor
    """

    follower = model.Person.test_or_set(database, message['follower']['id'])
    followed = model.Person.test_or_set(database, message['followed']['id'])

    if follower.is_remote:
        log.debug('Follower Actor cannot be remote.')
        return

    activitypub.Activity(
        type   = 'Follow',
        actor  = follower.uri,
        object = followed.uri,
        to     = followed.uri
    ).distribute()




# Ticket
# -----------------------------------------------------------------------------

@action('issue.new')
def new_issue(database, message):
    """
    A user has created a new Issue.
    """

    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['issue']['user']['name']) \
                .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == message['project']['id']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and tickettracker and ticket

    if tickettracker.is_remote:

        log.debug('Sending new Ticket to remote tracker.')

        # Get the JSONLD of the Ticket
        ticket_jsonld = activitypub.fetch(ticket.local_uri)

        activitypub.Activity(
            type   = 'Create',
            actor  = person.uri,
            to     = tickettracker.remote_uri,
            object = ticket_jsonld
        ).distribute()

    else:

        log.debug('Sending new Ticket to TicketTracker followers.')

        # Get the JSONLD of the TicketTracker
        tickettracker_jsonld = activitypub.fetch(tickettracker.local_uri)

        activitypub.Activity(
            type   = 'Create',
            actor  = person.uri,
            to     = tickettracker_jsonld['followers'],
            object = ticket.local_uri
        ).distribute()

@action('issue.comment.added')
def new_issue_comment(database, message):
    """
    A user has commented on an issue
    """

    # The Pagure notification contains *all* the comments of the issue
    # in an ordered list, so we need to extract the last one from the
    # list of comments.
    comment = database \
        .query(model.TicketComment) \
        .filter(model.TicketComment.id == message['issue']['comments'][-1]['id']) \
        .one_or_none()

    person = database \
                .query(model.Person) \
                .filter(model.Person.id == comment.user.id) \
                .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    # Our local ticket
    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    if tickettracker.is_remote:
        if not person.is_remote:
            log.debug('Sending new comment to remote tracker...')

            activitypub.Activity(
                type   = 'Create',
                actor  = person.uri,
                object = comment.uri,
                to     = tickettracker.uri
            ).distribute()

    else:
        log.debug('Sending new comment to TicketTracker followers.')

        # Retrieve the pagure "watchlist"
        watchlist = pagure.lib.query.get_watch_list(database, ticket)
        actors = []

        for username in watchlist:
            actor = database \
                .query(model.Person) \
                .filter(model.Person.user == username) \
                .one_or_none()

            actors.append(actor.uri)

        # Send the Activity
        activitypub.Activity(
            type   = 'Create',
            actor  = person.uri,
            object = comment.uri,
            to     = actors
        ).distribute()

@action('issue.edit')
def edit_issue(database, message):
    """
    A Pagure issue (Ticket) has been edited.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    assert project and tickettracker and ticket and person

    if 'status' in message['fields'] \
    and message['issue']['status'].upper() == 'CLOSED':
        """
        A user has closed an issue
        """

        # If the user has closed the Ticket of a remote tracker, we just
        # send the Activity to the tracker
        if ticket.is_remote:
            log.debug('Local user has closed the remote Ticket {}'.format(ticket.uri))

            activitypub.Activity(
                type = 'Resolve',
                actor = person.uri,
                object = ticket.remote_uri
            ).distribute()

        # otherwise we simply send the Activity to the Ticket's watchlist
        else:
            log.debug('Local user has closed the local Ticket {}'.format(ticket.uri))

            # Retrieve the pagure "watchlist"
            watchlist = pagure.lib.query.get_watch_list(database, ticket)
            actors = []

            for username in watchlist:
                actor = database \
                    .query(model.Person) \
                    .filter(model.Person.user == username) \
                    .one_or_none()

                actors.append(actor.uri)

            activitypub.Activity(
                type = 'Resolve',
                actor = person.uri,
                object = ticket.local_uri,
                to = actors
            ).distribute()

    if 'status' in message['fields'] \
    and message['issue']['status'].upper() == 'OPEN':
        """
        A user has reopened an issue
        """

        # If the user has opened the Ticket of a remote tracker, we just
        # send the Activity to the tracker
        if ticket.is_remote:
            log.debug('Local user has reopened the remote Ticket {}'.format(ticket.uri))

            activitypub.Activity(
                type = 'Reopen',
                actor = person.uri,
                object = ticket.remote_uri
            ).distribute()

        # otherwise we simply send the Activity to the Ticket's watchlist
        else:
            log.debug('Local user has reopened the local Ticket {}'.format(ticket.uri))

            # Retrieve the pagure "watchlist"
            watchlist = pagure.lib.query.get_watch_list(database, ticket)
            actors = []

            for username in watchlist:
                actor = database \
                    .query(model.Person) \
                    .filter(model.Person.user == username) \
                    .one_or_none()

                actors.append(actor.uri)

            activitypub.Activity(
                type = 'Reopen',
                actor = person.uri,
                object = ticket.local_uri,
                to = actors
            ).distribute()

    if 'milestone' in message['fields']:
        """
        A user has changed an issue's milestone
        """

        if ticket.is_remote:
            log.debug('User {} has changed milestone of the remote Ticket {}'.format(
                person.uri, ticket.uri))

            activitypub.Activity(
                type = 'Milestone',
                actor = person.uri,
                object = ticket.remote_uri,
                milestone = [ message['issue']['milestone'] ],
                to = tickettracker.uri
            ).distribute()
        else:
            log.debug('User {} has changed milestone of the local Ticket {}'.format(
                person.uri, ticket.uri))

            tickettracker_jsonld = activitypub.fetch(tickettracker.uri)

            activitypub.Activity(
                type = 'Milestone',
                actor = person.uri,
                object = ticket.local_uri,
                milestone = [ message['issue']['milestone'] ],
                to = tickettracker_jsonld['followers']
            ).distribute()

    if any(field in message['fields'] for field in ['title', 'content']):
        """
        A user has edited the content of the issue.
        """

        result = {}
        if 'title'   in message['fields']: result['summary'] = message['issue']['title']
        if 'content' in message['fields']: result['content'] = message['issue']['content']

        if ticket.is_remote:
            log.debug('Local user has edited the remote Ticket {}'.format(ticket.uri))

            activitypub.Activity(
                type = 'Update',
                actor = person.uri,
                object = ticket.remote_uri,
                to = tickettracker.remote_uri,
                result = result
            ).distribute()

        else:
            log.debug('Local user has edited the local Ticket {}'.format(ticket.uri))

            tickettracker_jsonld = activitypub.fetch(tickettracker.local_uri)

            if not tickettracker_jsonld:
                raise Exception('Cannot fetch TicketTracker {}'.format(ticket.local_uri))

            activitypub.Activity(
                type = 'Update',
                actor = person.uri,
                object = ticket.local_uri,
                to = tickettracker_jsonld['followers'],
                result = result
            ).distribute()

@action('issue.assigned.added')
def assign_issue(database, message):
    """
    A Pagure issue (Ticket) has a new assigned Person.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assignee = database \
        .query(model.Person) \
        .filter(model.Person.user == message['issue']['assignee']['name']) \
        .one_or_none()

    assert project and tickettracker and ticket and person and assignee

    if ticket.is_remote:

        log.debug('User {} has assigned {} to remote Ticket {}'.format(
            person.uri, assignee.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = assignee.uri,
            target = ticket.remote_uri,
        ).distribute()

    else:

        log.debug('User {} has assigned {} to local Ticket {}'.format(
            person.uri, assignee.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = assignee.uri,
            target = ticket.local_uri,
        ).distribute()

@action('issue.assigned.reset')
def unassign_issue(database, message):
    """
    A Pagure issue (Ticket) has removed the assigned Person.
    """

    log.debug('Not implemented. Have to change notification in Pagure first.')
    return

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and tickettracker and ticket and person

    if ticket.is_remote:

        log.debug('User {} has removed assignee from remote Ticket {}'.format(
            person.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = None,
            target = ticket.remote_uri,
        ).distribute()

    else:

        log.debug('User {} has removed assignee from local Ticket {}'.format(
            person.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = None,
            target = ticket.local_uri,
        ).distribute()

@action('issue.tag.added')
def tag_issue(database, message):
    """
    A Pagure issue (Ticket) has new tags.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and tickettracker and ticket and person

    tags = []
    for tag in message['tags']:
        tag = database \
            .query(model.Tag) \
            .filter(model.Tag.project_id == project.id,
                    model.Tag.tag == tag) \
            .one_or_none()

        if tag:
            tags.append(tag)

    for tag in tags:
        if ticket.is_remote:
            log.debug('User {} has added tags of remote Ticket {}'.format(person.uri, ticket.uri))

            activitypub.Activity(
                type = 'Add',
                actor = person.uri,
                object = tag.uri,
                target = ticket.remote_uri,
            ).distribute()

        else:

            log.debug('User {} has added tags to local Ticket {}'.format(person.uri, ticket.uri))

            activitypub.Activity(
                type = 'Add',
                actor = person.uri,
                object = tag.uri,
                target = ticket.local_uri
            ).distribute()

@action('issue.dependency.added')
def depend_issue(database, message):
    """
    A Pagure issue (Ticket) has new dependency.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.project_id == project.id,
                model.Ticket.id == message['added_dependency']) \
        .one_or_none()

    dependency = database \
        .query(model.Ticket) \
        .filter(model.Ticket.project_id == project.id,
                model.Ticket.id == message['issue']['id']) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and tickettracker and ticket and person and dependency

    if ticket.is_remote:
        log.debug('User {} has added dependency {} to remote Ticket {}'.format(
            person.uri, dependency.uri, ticket.uri))

        activitypub.Activity(
            type = 'Depend',
            actor = person.uri,
            object = ticket.remote_uri,
            target = dependency.uri
        ).distribute()

    else:

        log.debug('User {} has added dependency {} to local Ticket {}'.format(
            person.uri, dependency.uri, ticket.uri))

        activitypub.Activity(
            type = 'Depend',
            actor = person.uri,
            object = ticket.local_uri,
            target = dependency.uri
        ).distribute()

@action('issue.drop')
def drop_issue(database, message):
    """
    A Pagure issue (Ticket) was deleted.
    """

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == message['project']['id']) \
        .one_or_none()

    # We cannot query the database because the Issue has been deleted
    ticket = message['issue']

    # The user that edited the Ticket
    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    assert tickettracker and ticket and person

    if person.is_remote:
        log.debug('Ignoring notification triggered by remote action.')
        return

    # Is this a local or remote Ticket?
    if ticket['full_url'].startswith(APP_URL):
        tickettracker_jsonld = activitypub.fetch(tickettracker.uri)

        activitypub.Activity(
            type = 'Delete',
            actor = person.uri,
            object = ticket['full_url'],
            origin = tickettracker.uri,
            to = tickettracker_jsonld['followers']
        ).distribute()
    else:
        activitypub.Activity(
            type = 'Delete',
            actor = person.uri,
            object = ticket['full_url'],
            origin = tickettracker.uri,
            to = tickettracker.uri
        ).distribute()




# MergeRequest
# -----------------------------------------------------------------------------

@action('pull-request.new')
def new_merge_request(database, message):
    """
    A user has created a Pull Request in Pagure.
    """

    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['pullrequest']['user']['name']) \
                .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['pullrequest']['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and repository and mergerequest

    if repository.is_remote:
        log.debug('Sending new MergeRequest to remote repository...')

        activitypub.Activity(
            type   = 'Create',
            actor  = person.uri,
            to     = repository.remote_uri,
            object = mergerequest.local_uri
        ).distribute()

@action('pull-request.comment.added')
def new_merge_request_comment(database, message):
    """
    A user has created a new comment for a Pull Request.
    """

    # The Pagure notification contains *all* the comments of the pull
    # request in an ordered list, so we need to extract the last one from
    # the list of comments.
    comment = database \
        .query(model.MergeRequestComment) \
        .filter(model.MergeRequestComment.id == message['pullrequest']['comments'][-1]['id']) \
        .one_or_none()

    person = database \
                .query(model.Person) \
                .filter(model.Person.id == comment.user.id) \
                .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['pullrequest']['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    repository_jsonld = activitypub.fetch(repository.uri)

    # Our local pull-request
    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    assert comment and person and project and repository and mergerequest

    if repository.is_remote:
        if not person.is_remote:
            log.debug('Sending new comment to remote repository...')

            activitypub.Activity(
                type   = 'Create',
                actor  = person.uri,
                object = comment.uri,
                to     = repository.uri
            ).distribute()

    else:
        log.debug('Sending new comment to Repository followers.')

        """
        # Retrieve the pagure "watchlist"
        watchlist = pagure.lib.query.get_watch_list(database, mergerequest)
        actors = []

        for username in watchlist:
            actor = database \
                .query(model.Person) \
                .filter(model.Person.user == username) \
                .one_or_none()

            actors.append(actor.uri)
        """

        # Send the Activity
        activitypub.Activity(
            type   = 'Create',
            actor  = person.uri,
            object = comment.uri,
            to     = repository_jsonld['followers']
        ).distribute()

@action('pull-request.initial_comment.edited')
def edit_merge_request(database, message):
    """
    A user has edited the content of a MergeRequest.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['pullrequest']['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    # Our local pull-request
    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    assert project and repository and mergerequest and person

    result = {
        'summary': message['pullrequest']['title'],
        'content': message['pullrequest']['initial_comment']
    }

    if mergerequest.is_remote:
        log.debug('Local user has edited the remote MergeRequest {}'.format(mergerequest.uri))

        activitypub.Activity(
            type = 'Update',
            actor = person.uri,
            object = mergerequest.remote_uri,
            to = repository.remote_uri,
            result = result
        ).distribute()

    else:
        log.debug('Local user has edited the local MergeRequest {}'.format(mergerequest.uri))

        repository_jsonld = activitypub.fetch(repository.local_uri)

        if not repository_jsonld:
            raise Exception('Cannot fetch TicketTracker {}'.format(ticket.local_uri))

        activitypub.Activity(
            type = 'Update',
            actor = person.uri,
            object = mergerequest.local_uri,
            to = repository_jsonld['followers'],
            result = result
        ).distribute()

@action('pull-request.assigned.added')
def assign_pullrequest(database, message):
    """
    A Pagure pull request has a new assigned Person.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    # The user that edited the pull request
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assignee = database \
        .query(model.Person) \
        .filter(model.Person.user == message['pullrequest']['assignee']['name']) \
        .one_or_none()

    assert project and repository and mergerequest and person and assignee

    if mergerequest.is_remote:

        pass

    else:

        log.debug('User {} has assigned {} to MergeRequest {}'.format(
            person.uri, assignee.uri, mergerequest.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = assignee.uri,
            target = mergerequest.local_uri,
        ).distribute()

@action('pull-request.assigned.reset')
def unassign_pullrequest(database, message):
    """
    A Pagure issue (Ticket) has removed the assigned Person.
    """

    log.debug('Not implemented. Have to change notification in Pagure first.')
    return

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    tickettracker = database \
        .query(model.TicketTracker) \
        .filter(model.TicketTracker.id == project.id) \
        .one_or_none()

    ticket = database \
        .query(model.Ticket) \
        .filter(model.Ticket.id == message['issue']['id'],
                model.Ticket.project_id == project.id) \
        .one_or_none()

    # The user that edited the Ticket
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and tickettracker and ticket and person

    if ticket.is_remote:

        log.debug('User {} has removed assignee from remote Ticket {}'.format(
            person.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = None,
            target = ticket.remote_uri,
        ).distribute()

    else:

        log.debug('User {} has removed assignee from local Ticket {}'.format(
            person.uri, ticket.uri))

        activitypub.Activity(
            type = 'Assign',
            actor = person.uri,
            object = None,
            target = ticket.local_uri,
        ).distribute()

@action('pull-request.tag.added')
def tag_mergerequest(database, message):
    """
    A Pagure pull request has new tags.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    # The user that edited the pull request
    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    assert project and repository and mergerequest and person

    tags = []
    for tag in message['tags']:
        tag = database \
            .query(model.Tag) \
            .filter(model.Tag.project_id == project.id,
                    model.Tag.tag == tag) \
            .one_or_none()

        if tag:
            tags.append(tag)

    for tag in tags:
        if mergerequest.is_remote:
            log.debug('User {} has added tag {} to remote MergeRequest {}'.format(
                person.uri, tag.tag, mergerequest.uri))

            activitypub.Activity(
                type = 'Add',
                actor = person.uri,
                object = tag.uri,
                target = mergerequest.remote_uri,
            ).distribute()

        else:

            log.debug('User {} has added tag {} to local Ticket {}'.format(
                person.uri, tag.tag, mergerequest.uri))

            activitypub.Activity(
                type = 'Add',
                actor = person.uri,
                object = tag.uri,
                target = mergerequest.local_uri
            ).distribute()

@action('pull-request.closed')
def close_merge_request(database, message):
    """
    A user has closed a Pull Request. This notification does not distinguish between
    "Merged" and "Closed" (without merge). It's the same notification for both events
    so we have to look at the MergeRequest status.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['pullrequest']['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    # Our local pull-request
    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    assert project and repository and mergerequest and person

    if message['merged'] == True and mergerequest.is_remote:
        log.debug('Local user cannot merge the remote MergeRequest {}'.format(mergerequest.uri))

    if message['merged'] == False and mergerequest.is_remote:
        log.debug('Local user has closed the remote MergeRequest {}'.format(mergerequest.uri))

        activitypub.Activity(
            type = 'Close',
            actor = person.uri,
            object = mergerequest.local_uri,
            to = repository.remote_uri
        ).distribute()

    if message['merged'] == True and not mergerequest.is_remote:
        log.debug('Local user has merged the local MergeRequest {}'.format(mergerequest.uri))

        repository_jsonld = activitypub.fetch(repository.local_uri)

        activitypub.Activity(
            type = 'Resolve',
            actor = person.uri,
            object = mergerequest.local_uri,
            to = repository_jsonld['followers']
        ).distribute()

    if message['merged'] == False and not mergerequest.is_remote:
        log.debug('Local user has closed the local MergeRequest {}'.format(mergerequest.uri))

        repository_jsonld = activitypub.fetch(repository.local_uri)

        activitypub.Activity(
            type = 'Close',
            actor = person.uri,
            object = mergerequest.local_uri,
            to = repository_jsonld['followers']
        ).distribute()

@action('pull-request.reopened')
def reopen_merge_request(database, message):
    """
    A user has reopened a Pull Request.
    """

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['pullrequest']['project']['id']) \
        .one_or_none()

    repository = database \
        .query(model.Repository) \
        .filter(model.Repository.id == project.id) \
        .one_or_none()

    # Our local pull-request
    mergerequest = database \
        .query(model.MergeRequest) \
        .filter(model.MergeRequest.id == message['pullrequest']['id'],
                model.MergeRequest.project_id == project.id) \
        .one_or_none()

    person = database \
                .query(model.Person) \
                .filter(model.Person.user == message['agent']) \
                .one_or_none()

    assert project and repository and mergerequest and person

    if person.is_remote:
        log.debug('Remote user has reopened the MergeRequest {}'.format(mergerequest.uri))
        return

    if mergerequest.is_remote:
        log.debug('Local user has reopened the remote MergeRequest {}'.format(mergerequest.uri))

        activitypub.Activity(
            type = 'Reopen',
            actor = person.uri,
            object = mergerequest.remote_uri,
            to = repository.remote_uri
        ).distribute()

    if not mergerequest.is_remote:
        log.debug('Local user has reopened the local MergeRequest {}'.format(mergerequest.uri))

        repository_jsonld = activitypub.fetch(repository.local_uri)

        activitypub.Activity(
            type = 'Reopen',
            actor = person.uri,
            object = mergerequest.local_uri,
            to = repository_jsonld['followers']
        ).distribute()




# Groups and Roles
# -----------------------------------------------------------------------------

@action('project.user.added')
def add_user(database, message):
    """
    A user has been added to a Pagure project.
    """

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    new_member = database \
        .query(model.Person) \
        .filter(model.Person.user == message['new_user']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.Role) \
        .filter(model.Role.project_id == project.id,
                model.Role.user_id    == new_member.id,
                model.Role.access     == message['access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and new_member and role

    if project.is_local:

        log.debug('{} has added user {} to role {}'.format(person.uri, new_member.uri, role.uri))

        activitypub.Activity(
            type    = 'Add',
            actor   = person.uri,
            object  = new_member.uri,
            target  = role.uri,
            context = project.uri
        ).distribute()

@action('project.user.access.updated')
def update_user_access(database, message):
    """
    A user access level has been updated.
    """

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    member = database \
        .query(model.Person) \
        .filter(model.Person.user == message['new_user']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.Role) \
        .filter(model.Role.project_id == project.id,
                model.Role.user_id    == member.id,
                model.Role.access     == message['new_access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and member and role

    if project.is_local:

        log.debug('{} has updated user {} to role {}'.format(person.uri, member.uri, role.uri))

        activitypub.Activity(
            type    = 'Update',
            actor   = person.uri,
            object  = member.uri,
            result  = { 'roles': [ role.uri for role in member.roles ] }
        ).distribute()

@action('project.user.removed')
def remove_user(database, message):
    """
    A user has been removed from a Pagure project.
    """

    log.debug('Waiting for https://pagure.io/pagure/pull-request/5156 to be '
              'merged before letting this through.')
    return

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    new_member = database \
        .query(model.Person) \
        .filter(model.Person.user == message['removed_user']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.Role) \
        .filter(model.Role.project_id == project.id,
                model.Role.user_id    == new_member.id,
                model.Role.access     == message['access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and role

    if project.is_local:

        log.debug('{} has removed user {} from role {}'.format(person.uri, new_member.uri, role.uri))

        activitypub.Activity(
            type    = 'Remove',
            actor   = person.uri,
            object  = new_member.uri,
            target  = role.uri,
            context = project.uri
        ).distribute()

@action('project.group.added')
def add_group(database, message):
    """
    A group has been added to a Pagure project.
    """

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    new_group = database \
        .query(model.Group) \
        .filter(model.Group.group_name == message['new_group']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.ProjectRole) \
        .filter(model.ProjectRole.project_id == project.id,
                model.ProjectRole.group_id   == new_group.id,
                model.ProjectRole.access     == message['access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and new_group and role

    if project.is_local:

        log.debug('{} has added group {} to role {}'.format(person.uri, new_group.uri, role.uri))

        activitypub.Activity(
            type    = 'Add',
            actor   = person.uri,
            object  = new_group.uri,
            target  = role.uri,
            context = project.uri
        ).distribute()

@action('project.group.access.updated')
def update_group_access(database, message):
    """
    A group access level has been updated.
    """

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    group = database \
        .query(model.Group) \
        .filter(model.Group.group_name == message['new_group']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.ProjectRole) \
        .filter(model.ProjectRole.project_id == project.id,
                model.ProjectRole.group_id   == group.id,
                model.ProjectRole.access     == message['new_access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and group and project and role

    if project.is_local:

        log.debug('{} has updated group {} to role {}'.format(person.uri, group.uri, role.uri))

        activitypub.Activity(
            type    = 'Update',
            actor   = person.uri,
            object  = group.uri,
            result  = { 'roles': [ role.uri for role in group.roles ] }
        ).distribute()

@action('project.group.removed')
def remove_group(database, message):
    """
    A user has been removed from a Pagure project.
    """

    log.debug('Waiting for https://pagure.io/pagure/pull-request/5156 to be '
              'merged before letting this through.')
    return

    person = database \
        .query(model.Person) \
        .filter(model.Person.user == message['agent']) \
        .one_or_none()

    new_member = database \
        .query(model.Person) \
        .filter(model.Person.user == message['removed_user']) \
        .one_or_none()

    project = database \
        .query(model.Project) \
        .filter(model.Project.id == message['project']['id']) \
        .one_or_none()

    role = database \
        .query(model.Role) \
        .filter(model.Role.project_id == project.id,
                model.Role.user_id    == new_member.id,
                model.Role.access     == message['access']) \
        .one_or_none()

    # This should never raise an error otherwise there's a bug in Pagure
    assert person and project and role

    if project.is_local:

        log.debug('{} has removed user {} from role {}'.format(person.uri, new_member.uri, role.uri))

        activitypub.Activity(
            type    = 'Remove',
            actor   = person.uri,
            object  = new_member.uri,
            target  = role.uri,
            context = project.uri
        ).distribute()

